0%

用 C# .NET Core 爬取每月財報 | 用程式打造選股策略(4)

前言

上一篇 用 C# .NET Core 爬取每季財報 已經抓取了每季財報
不過由於季報週期太長,因此通常還需要月報來確認每個月的營收是否有符合預期
那這篇就來做每月財報資訊的爬蟲吧!

觀察網站

和季報一樣,我們可以從 公開資訊觀測站 找尋我們要的資料

從 首頁 > 彙總報表 > 營運概況 > 每月營收 > 採用IFRSs後每月營業收入彙總表

選擇時間後按下查詢

接著我們會到財報頁面,如果要爬這一頁也是可以
但馬上發現可以下載CSV,當然馬上選CSV,處理起來會方便一些

打開 Chrome -> F12,按下下載,觀察Request

發現請求相當單純,從fileName發現 _108_1就是表示108年1月,
因此我們只要調整這個參數抓取我們要的月份資料就可以了,

觀察完畢,馬上開始動工!

爬取財報資訊

爬蟲部分

爬取CSV,直接讀成string型態回傳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public async Task<string> ReadMonthReportCsvByTWSEAsync(int year, int month)
{
using(var client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
var response = await client.PostAsync(
"https://mops.twse.com.tw/server-java/FileDownLoad",
new StringContent($"step=9&functionName=show_file&filePath=%2Fhome%2Fhtml%2Fnas%2Ft21%2Fsii%2F&fileName=t21sc03_{year}_{month}.csv")
);
var result = await response.Content.ReadAsStringAsync();
if(response.StatusCode != System.Net.HttpStatusCode.OK)
throw new PlatformNotSupportedException($"目前無法爬取財報資料...,{response.StatusCode}{result}");
return result;
}
}

解析CSV資料

接下來解析 CSV 檔案,這裡我使用 CsvHelper 這個套件
安裝套件:

1
dotnet add package CsvHelper --version 15.0.5

針對資料欄位我們先建立一個 DB Model,
資料庫就自己去建吧!這裡就不多說了
[Name]這個 attribute 是 CsvHelper 會針對欄位名稱來自動轉換所使用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Table("MonthReport")]
public class MonthReport
{
[ExplicitKey]
[Name("公司代號")]
public string stock_id { get; set; }
[ExplicitKey]
public int year { get; set; }
[ExplicitKey]
public int month { get; set; }
[Name("營業收入-當月營收")]
public string revenue { get; set; }
[Name("備註")]
public string remark { get; set; }
}

這個 Model 因為 year 跟 month 在 CSV 內並沒有資料,是我們自己傳入的!所以這裡需要排除這個兩個欄位
根據 CsvHelper官方文件,我們可以繼承 ClassMap 來將不要的欄位 Ignore 掉!

1
2
3
4
5
6
7
8
9
public class MonthReportMap: ClassMap<MonthReport>
{
public MonthReportMap()
{
AutoMap(CultureInfo.InvariantCulture);
Map(m => m.year).Ignore();
Map(m => m.month).Ignore();
}
}

然後我們將剛剛的string傳入,
並且直接讀成IEnumerable<MonthReport>
這樣就可以等等就可以直接存進資料庫!

1
2
3
4
5
6
7
8
9
public IEnumerable<MonthReport> ReadCsv(string data)
{
using (var reader = new StringReader(data))
using (var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csvReader.Configuration.RegisterClassMap<MonthReportMap>();
return csvReader.GetRecords<MonthReport>().ToList();
}
}

排程 & 存入資料庫

接著處理資料庫的部分,沒什麼複雜的東西,直接看程式碼吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MonthReportRepository
{
private readonly SqlConnection _conn;
private readonly ILogger<MonthReportRepository> _logger;
public MonthReportRepository(ILogger<MonthReportRepository> logger, SqlConnection conn)
{
_logger = logger;
_conn = conn;
}

public void Insert(IEnumerable<MonthReport> monthReportList)
{
try
{
using(var scope = new TransactionScope())
{
foreach(var month in monthReportList)
{
_conn.Insert(month);
}
scope.Complete();
}
}
catch(Exception ex)
{
_logger.LogError(ex.Message);
}
}

internal bool IsExist(int year, int month)
{
return _conn.ExecuteScalar<bool>("select count(1) from MonthReport where year=@year and month=@month", new { year, month });
}

public int GetMaxMonth(int year)
{
return _conn.ExecuteScalar<int>("select isnull(max(month), 1) from MonthReport where year=@year", new { year });
}

public int GetMaxYear()
{
return _conn.ExecuteScalar<int>("select isnull(max(year), 106) from MonthReport");
}
}

最後,整理成一個 method ,方便 schedule 呼叫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public async Task ExecuteAsync(int year, int month)
{
if(_monthReportRepository.IsExist(year, month)) {
_logger.LogDebug($"{year}, {month} data is exist");
return;
}
_logger.LogInformation($"MonthReportClawer Running, year = {year}, month = {month}");
string data = await ReadMonthReportCsvByTWSEAsync(year, month);
var reports = ReadCsv(data).Select(report => {
report.year = year;
report.month = month;
return report;
});
_monthReportRepository.Insert(reports);
}

到這邊爬蟲的部分已經完成,接下來就來設定排程,
一樣使用 Coravel 這個套件,這邊就不多說明了!不懂的朋友可以回去看這篇 => 用 C# .NET Core 自動爬取台股每日股價

這邊從 106年1月 開始爬資料,和季報一樣,在上面SQL中,已經技巧性的處理掉了MaxMonth、MaxYear
接著直接看Code吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MonthReportClawerSchedule : IInvocable
{
private MonthReportClawer _monthReportClawer;
private MonthReportRepository _monthReportRepository;
public MonthReportClawerSchedule (MonthReportClawer monthReportClawer, MonthReportRepository monthReportRepository)
{
_monthReportClawer = monthReportClawer;
_monthReportRepository = monthReportRepository;
}

public async Task Invoke()
{
int year = _monthReportRepository.GetMaxYear();
int month = _monthReportRepository.GetMaxMonth(year);
int twNowYear = DateTime.Now.Year-1911;
while(year <= twNowYear)
{
while((year < twNowYear && month <= 12) || (year == twNowYear && month <= DateTime.Now.Month))
{
await _monthReportClawer.ExecuteAsync(year, month);
Thread.Sleep(7000);
month++;
}
year++;
month = 1;
}
}
}

最後,註冊我們的排程:

1
2
3
scheduler
.Schedule<MonthReportClawerSchedule>()
.Cron("30 8 10 * *");

搞定收工!

心得

這次爬月報還是比較簡單的,
有了月報之後就可以透過程式自己找出月營收成長的股票,
現在已經有了股價,可以做技術分析,
又有了季報、月報,可以做基本面分析,
好像還缺了籌碼分析,那下一篇,就來爬千張大戶的籌碼吧,敬請期待!

↓↓↓ 如果喜歡我的文章,可以幫我按個Like! ↓↓↓
>> 或者,請我喝杯咖啡,這樣我會更有動力唷! <<<
街口支付

街口支付

街口帳號: 901061546

歡迎關注我的其它發布渠道